﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal class DataRangeEnumerator : DataRangeEnumeratorBase
  {
    #region Static Fields

    private static readonly Guid GetNextCallId = DataUtils.CreateCallId();

    #endregion

    #region Constructor

    internal DataRangeEnumerator(
      IEnumerable<DataRange> ranges,
      Func<GroupHandler, SortingHandler, FilteringHandler, IAsyncEnumerator<DataPath>> enumeratorCreator )
    {
      if( enumeratorCreator == null )
        throw new ArgumentNullException( "enumeratorCreator" );

      // Set the ranges to apply on the data source.
      m_ranges = ( ranges != null )
                   ? new LinkedList<DataRange>( ranges )
                   : new LinkedList<DataRange>();
      m_enumeratorCreator = enumeratorCreator;
    }

    #endregion

    #region HasMore Protected Property

    protected override bool HasMore
    {
      get
      {
        return ( !m_disposed )
            && ( !m_isAfterLast )
            && ( m_ranges != null )
            && ( m_ranges.Count > 0 );
      }
    }

    #endregion

    public override void Reset()
    {
      this.CheckState();

      // Get back at start.
      m_isAfterLast = false;
      m_currentRange = null;
      m_currentPosition = null;

      if( m_currentEnumerator != null )
      {
        m_currentEnumerator.Dispose();
        m_currentEnumerator = null;
      }
    }

    protected override void Dispose( bool disposing )
    {
      base.Dispose( disposing );

      m_currentRange = null;

      if( m_currentEnumerator != null )
      {
        m_currentEnumerator.Dispose();
        m_currentEnumerator = null;
      }

      if( m_ranges != null )
      {
        m_ranges.Clear();
      }
      m_ranges = null;

      m_currentPosition = null;

      m_disposed = true;
    }

    protected override IAsyncResult BeginGetNextCore( AsyncCallback callback, object state )
    {
      AsyncResult asyncResult = new AsyncResult( DataRangeEnumerator.GetNextCallId, callback, state );

      // We already know there is no more item.
      if( !this.HasMore )
      {
        asyncResult.StartSync( new GetNextResult() );
      }
      else
      {
        // Get the first range.
        if( m_currentRange == null )
        {
          m_currentRange = m_ranges.First;
        }

        // Retrieve the current range.
        DataRange range = m_currentRange.Value;
        long moveCount = 1;

        // We have to create the enumerator for the current range.
        if( m_currentEnumerator == null )
        {
          DataRangeContext context = range.Context;

          GroupHandler groupingHandler = new GroupHandler();
          groupingHandler.GroupCountPolicy = GroupCountPolicy.DoNotHandleCounts;

          // Find out the sort to apply on the data source.
          SortingHandler sortingHandler = new SortingHandler();
          foreach( SortDescription sort in context.SortDescriptions )
          {
            sortingHandler.SortDescriptions.Add( sort );
          }

          // Find out the filer to apply on the data source.
          FilteringHandler filteringHandler = new FilteringHandler();
          filteringHandler.Filter = context.Filter;
          filteringHandler.FilterExpression = context.FilterExpression;

          // Create the enumerator.
          m_currentEnumerator = m_enumeratorCreator.Invoke( groupingHandler, sortingHandler, filteringHandler );
          m_currentPosition = range.StartPosition;

          // The first item to get in the range may be included, so we must not skip it.
          if( !m_currentPosition.IsOutOfBound )
          {
            moveCount = 0;
          }
        }

        JumpMoveAndFetchParameters parameters = new JumpMoveAndFetchParameters( m_currentPosition, moveCount, 1 );

        asyncResult.Start(
          this.BeginGetNextItem( parameters ),
          this.EndGetNextItem );
      }

      return asyncResult;
    }

    protected override GetNextResult EndGetNextCore( IAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( DataRangeEnumerator.GetNextCallId );

      return asyncResult.GetCallResult<GetNextResult>();
    }

    private Func<AsyncCallback, object, IAsyncResult> BeginGetNextItem(
      JumpMoveAndFetchParameters parameters )
    {
      return ( cb, st ) => m_currentEnumerator.BeginJumpMoveAndFetch( parameters, cb, st );
    }

    private DependentCallResult EndGetNextItem(
      AsyncResult parentAsyncResult,
      IAsyncResult asyncResult )
    {
      if( !this.HasMore )
        return new DependentCallResult( new GetNextResult() );

      // Retrieve the item.
      JumpMoveAndFetchResult<DataPath> jumpResult = m_currentEnumerator.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )asyncResult );
      DataPath dataPath = DataRangeEnumerator.GetFirst( jumpResult.FetchedItems );
      bool hasMore = false;

      m_currentPosition = jumpResult.EndPositionObject;
      DataRange currentRange = m_currentRange.Value;

      if( dataPath != null )
      {        
        bool isAtStartPosition = ( currentRange.StartPosition == jumpResult.SourcePosition );
        bool isInsideRange = !currentRange.IsOutside( dataPath );

        // The start position is bringing us on the border of the range.
        // The first fetched item may be out of the range if the range start 
        // is excluded.
        hasMore = ( isInsideRange || isAtStartPosition );

        // Make sure the item is one of the desired item and hasn't been returned yet.
        if( ( !isInsideRange )
          || ( currentRange.IsFilteredOut( dataPath ) )
          || ( !DataRangeEnumerator.IsNewDataPath( dataPath, currentRange.IsSelectionRangeInclusive, m_currentRange.Previous ) ) )
        {
          dataPath = null;
        }

        bool isTargetingSingleItem = ( currentRange.StartPosition == currentRange.EndPosition );

        if( isTargetingSingleItem )
        {
          // We know the range is targeting a single item.  It is useless to query for
          // the item that comes next.  To prevent the query to occur on the data source, we
          // set the end of the source as the next starting position.
          m_currentPosition = SourcePosition.AfterLast;
        }
      }

      // We have exhausted the current range, we'll start fetching on the next range.
      if( !hasMore )
      {
        m_currentEnumerator.Dispose();
        m_currentEnumerator = null;
        m_currentRange = m_currentRange.Next;
      }

      // The current data range is null if there is no more range.
      if( ( dataPath != null ) || ( m_currentRange == null ) )
      {
        // We have fetched the last item.
        m_isAfterLast = ( m_currentRange == null );

        return new DependentCallResult( new GetNextResult( hasMore, dataPath, currentRange.IsSelectionRangeInclusive ) );
      }

      // We have to fetch on the next range to get the desired item.
      return new DependentCallResult(
        null,
        this.BeginGetNextCore,
        ( par, iar ) => new DependentCallResult( this.EndGetNextCore( iar ) ) );
    }

    private static bool IsNewDataPath( DataPath dataPath, bool isDataPathRangeInclusive, LinkedListNode<DataRange> range )
    {
      if( dataPath == null )
        return false;

      while( range != null )
      {
        // The datapath is already included in a previous range
        if( range.Value.Contains( dataPath ) )
        {
          // If the range of the current datapath is the same as the range
          // of the previous datapath, we don't have a new Datapath; however, 
          // if they are different it means that the datapath was 
          // added within a selection range and will added as an 
          // unselected range.
          if( isDataPathRangeInclusive == range.Value.IsSelectionRangeInclusive )
          {
            return false;
          }
          else
          {
            return true;
          }
        }

        range = range.Previous;
      }

      return true;
    }

    private static T GetFirst<T>( IEnumerable<T> enumerable )
    {
      return ( enumerable != null )
               ? enumerable.FirstOrDefault()
               : default( T );
    }

    #region Private Fields

    private LinkedList<DataRange> m_ranges; //null
    private LinkedListNode<DataRange> m_currentRange; //null
    private IAsyncEnumerator<DataPath> m_currentEnumerator; //null
    private SourcePosition m_currentPosition; //null

    private bool m_isAfterLast; //false
    private bool m_disposed; //false

    Func<GroupHandler, SortingHandler, FilteringHandler, IAsyncEnumerator<DataPath>> m_enumeratorCreator; //null

    #endregion
  }
}
